import re
import os
import sys
import json
import logging

class ModuleBuilder():
  """The class which parses a module configuration file and builds/executes
  the command to compile using the Closure compiler."""
  
  def __init__(self, modulesFileName):
    self.modFileName = modulesFileName
    
  def parseModules(self):
    "Read and parse the 'modules.js' config file in the current directory"
    
    moduleFile = open(self.modFileName, 'r')
    
    # The first line is filler to allow the file to validate as javascript
    # Read the first line, strip the trailing newline, and get just the '['
    modules = moduleFile.readline().strip()[-1:]
    
    # Read in and concatenate the lines from moduleFile
    modules = ("%s%s" % (modules, ''.join([line.strip()
                                      for line in moduleFile.readlines()])))
    
    moduleFile.close()
    
    # Ignore the trailing semicolon if one exists
    if modules[-1] == ';':
      self.modules = json.loads(modules[:-1])
    else:
      self.modules = json.loads(modules)
      
    # Create a map of module names to module objects to prevent repeated array
    # traversals.
    self.moduleMap = {}
    
    for module in self.modules:
      self.moduleMap[module["name"]] = module
  
  # Helper method for MarkModuleDependencies
  # Recursively builds an array of dependent modules for each module.
  def markDependency(self, moduleName, dependencies, level):
    "Mark out a module's dependency path."
    
    for dependency in dependencies:
      
      depMod = self.moduleMap[dependency]
      if "dependents" not in depMod:
        depMod["dependents"] = []
      
      depObj = {
        "dependentName": moduleName,
        "level": level
      }
      
      depMod["dependents"].append(depObj)
      self.markDependency(moduleName, depMod["dependentOn"], level + 1)
  
  # Iterate through the modules array and recursively mark out each module's 
  # path up the dependency tree all the way to the root module.
  def markModuleDependencies(self):
    "Traverse modules array to mark module dependents"
    
    # For each module, traverse its dependencies and mark them.
    for module in self.modules:
      self.markDependency(module["name"], module["dependentOn"], 1)
  
  def mapNamespacesToModules(self):
    "Map the user-defined namespaces to the modules in which they're defined."
    
    namespaceModules = {}
    
    # self.modules[i]["namespaces"] is an array of namespaces in that module.
    for module in self.modules:
      namespaces = module["namespaces"]
      
      for namespace in namespaces:
        # If the namespace is already in namespaceModules, then there's a problem
        # No namespace can exist in two different modules
        if namespace in namespaceModules:
          logging.error("Namespace %s exists in multiple modules." % namespace)
        else:
          namespaceModules[namespace] = module["name"]
    
    self.namespaceModules = namespaceModules
  
  def mapNamespaceDependencies(self):
    "Maps namespaces to their dependencies, both goog.* and user-defined."
    
    CLOSURE = "../Closure/closure/bin/build"
    
    # Build the dependencies file
    createDepFile = "%s%s%s%s" % ("python ", CLOSURE,
                                "/depswriter.py --root=../goog/",
                                " --root=src/ --output_file=deps/temp_deps.js")
    os.system(createDepFile)
    
    # A map of namespaces to their filename.
    namespaceFilenames = {}
    
    # A map of namespaces to an array of the namespaces they require.
    namespaceDependencies = {}
    
    # RegExp pattern that matches a goog.addDependency call to extract
    # the file name, provided and required namespaces.
    # goog.addDependency('goog\useragent\useragent.js',
    #                                     ['goog.userAgent'], ['goog.string']);
    pattern = re.compile(r"""^goog\.addDependency\(
                      (?P<filename>"\S*?\.js"),\ # Capture the filename
                      (?P<provided>\[.*?\]),\ # Capture the provided namespaces
                      (?P<required>\[.*?\]) # Capture the required namespaces
                      \);""", re.X)
  
    # Open the deps file and parse it.
    depsFile = open('deps/temp_deps.js')
    for dep in depsFile:
      # Strip whitespace and convert any single quotes to double quotes to
      # allow the array strings to be easily parsed via JSON.loads()
      dep = dep.strip().replace(r"'", r'"')
      
      matches = pattern.match(dep)
      
      # The first two lines of the file should fail, since those are the
      # "autogenerated, do not delete" comments.
      if matches != None:
        filename = matches.group('filename') # File name string
        provided = json.loads(matches.group('provided')) # Provided namespaces
        required = json.loads(matches.group('required')) # Required namespaces
        
        for namespace in provided:
          namespaceFilenames[namespace] = filename
          namespaceDependencies[namespace] = required
    
    depsFile.close()
    os.remove('deps/temp_deps.js')
    
    self.namespaceDependencies = namespaceDependencies
    self.namespaceFilenames = namespaceFilenames

  # Parse the goog.* namespace dependency tree for each module and set up a
  # map from goog.* namespaces to the modules that require them.
  def calcGoogNSByModule(self):
    # Map of goog namespaces to the modules in which they're required.
    self.googNSByModules = {}
    
    for module in self.modules:
      self.parseDepsTree(module["namespaces"], module)
    
    # parseDepsTree does not filter out duplicate dependencies between modules.
    # We need to do that manually.
    self.resolveDuplicates()

  # Helper function for calcGoogNSByModule
  # Recursively delve into the dependency tree for each user-defined namespace
  # and aggregate the required goog.* namespaces for this module
  def parseDepsTree(self, namespaces, module):
    "Recursively parse the dependency lists for a list of goog.* namespaces"
    
    for namespace in namespaces:
      myDeps = self.namespaceDependencies[namespace]
      if len(myDeps) > 0:
        self.parseDepsTree(myDeps, module)
        
      if (namespace[:4] == "goog"):
        module["googDependencies"].append(namespace)
        if namespace not in self.googNSByModules:
          self.googNSByModules[namespace] = []
        
        if module["name"] not in self.googNSByModules[namespace]:
          self.googNSByModules[namespace].append(module["name"])

  # For goog.* namespaces required in multiple modules, move them to the
  # ancestor module closest to the nodes in the dependency tree that use them.
  def resolveDuplicates(self):
    """Make sure goog namespaces required in multiple modules are in a
    common parent."""
    
    # googNS is the name of a goog.* namespace
    # googNSByModules[googNS] is an array of module names that use googNS
    for googNS in self.googNSByModules:
      moduleNames = self.googNSByModules[googNS]
      if len(moduleNames) == 1:
        moduleMap[moduleNames[0]]
      
      pass
  
  def buildModuleDependencyFiles(self):
    "Generate each module's '<module_name>_deps.js' file"
    
    for module in self.modules:
      logging.info("  Generating '%s_deps.js'..." % module["name"])
      namespaces = module["namespaces"]
      
    # Concatted files
    # Keep track of which Closure source files are concatenated into the script
    # This is necessary for goog.events and goog.events.EventType since
    # they can be goog.require()'ed separately but reside in the same file.
    self.concattedFiles = []
  
    # Build a module_deps.js file for each module that only includes goog.*
    # namespaces.  This must be a separate iteration because the combination
    # of goog namespaces can only happen after required namespaces have been
    # moved to shared dependencies to avoid re-inclusion.
    # for module in self.modules:
    #  self.generateModuleDepsFile(module)
  
  def generateModuleDepsFile(self, module):
    "Concatenate all of the goog.* dependencies into '<module_name>_deps.js'"
    
    depsFile = open("deps/%s_deps.js" % module["name"], "w")
    
    for namespace in module["googDependencies"]:
      filename = (self.namespaceFilenames[namespace]).strip('"')
      if filename not in self.concattedFiles:
        namespaceFile = open(r"..\goog\%s" % filename)
        for line in namespaceFile:
          depsFile.write(line)
        
        namespaceFile.close()
        self.concattedFiles.append(filename)
    
    depsFile.close()
  
  def compile(self):
    "Compile your application."
    
    compilerHead = "%s%s%s%s" % ("java -jar",
                             " ../Closure/closure/bin/build/compiler.jar",
                             " --compilation_level=ADVANCED_OPTIMIZATIONS",
                             " --js ../goog/base.js")
    
    compilerInputs = []
    
    # Start with goog/base.js
    moduleFiles = 1
    for module in self.modules:
      moduleFiles += 1 # start out with at least the dependency file.
      namespaces = module["namespaces"]
      compilerInputs.append(" --js deps\%s_deps.js" % module["name"])
      
      for namespace in namespaces:
        compilerInputs.append(" --js src\%s" % (
                                self.namespaceFilenames[namespace].strip('"')))
        moduleFiles += 1
      
      compilerInputs.append(" --module %s:%d" % (module["name"], moduleFiles))
      if(len(module["dependentOn"]) > 0):
        compilerInputs.append(':')
        compilerInputs.append(','.join(module["dependentOn"]));
      
      moduleFiles = 0 # Clear for the next iteration
    
    compilerInputs.append(" --module_output_path_prefix compiled/")
    compilerInputs.append(" --jscomp_off=fileoverviewTags")
    compilerInputs.append(" --formatting=PRETTY_PRINT")
    
    compilerCommand = "%s%s" % (compilerHead, ''.join(compilerInputs))
    logging.info("")
    logging.info("Compiling with command:")
    logging.info(compilerCommand)
    os.system(compilerCommand)

def main():
  logging.basicConfig(format=(sys.argv[0] + ': %(message)s'),
                      level=logging.INFO)
  
  # Kick off the module builder on program start-up.
  # Eventually we'll implement a command-line arguments parser and feed those
  # to the ModuleBuilder instance.
  builder = ModuleBuilder("modules.js")
  
  # Fill the modules array
  logging.info("Parsing modules.js...")
  builder.parseModules()
  
  # Map out the inter-dependencies among modules
  # Used for resolving goog.* namespaces required in multiple modules
  builder.markModuleDependencies()
  
  # Map user-defined namespaces to the module in which they are provided.
  logging.info("Scanning source code for dependency data...")
  self.mapNamespacesToModules()
  
  # Build the dependency list for our app using the depswriter.py utility.
  # Sets up self.namespaceDependencies - a dict of namespaces to requirements.
  logging.info("Building namespace dependency tree...")
  builder.mapNamespaceDependencies()
  
  # Map each required goog.* namespace to the modules that use them
  builder.calcGoogNSByModule()
  
  # Build the dependency files for each module.
  logging.info("Building dependency files for each module.")
  builder.buildModuleDependencyFiles()
  
  # Compile the application
  builder.compile()
  
  
  
if __name__ == "__main__":
  main()